Skip to content

v0.23.0 - #498

Merged
soronpo merged 8 commits into
mainfrom
training
Aug 24, 2026
Merged

v0.23.0#498
soronpo merged 8 commits into
mainfrom
training

Conversation

@soronpo

@soronpo soronpo commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

soronpo and others added 8 commits August 18, 2026 00:42
…atch

The three bits range-selection givens (the low-0 form, the width+low form, and
the H-form for the annotation path) collapse into one over `DFBitsWL[W, L2]`
receivers. What kept them apart was never the receiver shapes: a
`CheckNUB[HI, HighIdx[W, L2]]` bound routes the fold application through
`UBound.Aux`, where the const-guard's reduction is context-dependent. It can
collapse to `Int` inside the implicit search while the same application reduces
to a literal in the required type, so the found candidate no longer conforms:
a real violation surfaced as a raw given-mismatch dump instead of the check's
message, and a valid selection on an annotation-path receiver could fail to
resolve outright (the reason the H-form existed).

`IntP.HighIdxOf` now makes the literal-vs-wide decision by given prioritization
instead of a guard: the literal instance computes `W + L - 1` in raw
`compiletime.ops` over `Int & Singleton` arguments, the wide instance answers
`Int`, and the op given feeds the check a plain, already-decided type parameter.
Static checks fire wherever the receiver's bounds reduce, degrade to their
elaboration-time half where they do not, and a static failure renders the real
message: an out-of-range selection on a `BitsHL[9, 2] <> VAL` struct field now
reports "Index 15 is above the high index 9 of the selected value" at the
user's expression, identical to the direct-declaration case.

Fixes #488

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Porting VeeR's rvdff_fpga needs a cell whose clock input the baseline calls
`rawclk` while a second, dead input is called `clk`. That works, and the
mechanism is worth writing down: @timing.clock(portName = ...) renames the
port, and the parent still binds its own clock to it, because the magnet
matches by domain rather than by name. The domain then propagates down, so a
grandchild is emitted with the renamed port too.

Two traps come with it. A renamed-clock design must stay childless, or the
emission is invalid SystemVerilog: duplicated rst_l_0/rst_l_1 ports against a
.rst_l connection, plus a hierarchical assign through the module type name.
It elaborates clean and only slang catches it, so run the emitted files
through slang after a rename. And `val clk = Clk <> IN` silently wins over
portName, dropping the renamed port and moving the registers onto whatever the
parent wires to `clk` -- so the second clock input has to be a plain Bit,
which the magnet leaves alone despite the name.

Also corrects the clock-only-annotation bullet, which is expected behaviour
(a domain with an init and no reset signal) rather than the bug it reads as,
and whose emitted form has drifted to an `initial` block. Restate @..reset
alongside a rename when the module does reset.

The general half of the rename behaviour is marked as a user-guide doc gap.

Submodule: benchmarks moves to the upstream-copyright sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`x := Vector.fill(16)(1)` and `x := Vector(0, 1, 1, ...)` were rejected for a
`Bit X 16` receiver. Scala widens the singleton literal types when it infers a
collection's element type, so both come out as `Vector[Int]`, and `Int` is no
candidate for a `Bit` cell: only `BitNum` is.

Exact already restores exact types term by term, so it is where the widening is
undone. A new `asBitCollection` step retypes a collection as `C[BitNum]` when
the application's LAST argument list holds nothing but 0 or 1 literals. Reading
the last list is what covers both forms: for `Vector(0, 1, ...)` it is the
vararg `Repeated`, and for `Vector.fill(16)(1)` it is the element argument, so
the length never enters the decision. `Vector.tabulate(16)(f)`, and any literal
that is neither 0 nor 1, do not match and pass through untouched.

`BitNum` stays a subtype of `Int`, so a collection headed for a decimal vector
is unaffected: `IntInfo` reads the union exactly as it reads `Int`, and
`Vector(0, 1, 0, 1)` into a `UInt(8) X 4` still yields 8-bit constants. The
retyping is restricted to an `Iterable`, where the element type erases to a
reference and the cast is a runtime no-op; an `Array`, whose element type
survives erasure, is deliberately left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t is anonymous

`MetaContextGenPhase` lets an owner-less apply keep the context propagated into it,
rather than minting an anonymous one, whenever the applied function's name carries a
`$`: proxies, anonymous functions and default getters all build a value that belongs
to the context around them.

An inline accessor (`inline$foo`) carries one too, and it should not. The compiler
mints it only so an inline expansion can reach a member it cannot name directly, so
it stands for the user-written call. The `b`/`h` interpolators expand to
`DFBits.StrInterp.inline$interpolate`, which meant an anonymous interpolated constant
kept the enclosing design's own context, and a design's context is named after the
instance val in its parent:

    class Foo extends RTDesign:
      val movecircle = new Bar
      ...
    class Bar extends RTDesign:
      val x = corners == b"4'1001"   // printed as `val movecircle: Bits[4] <> CONST`

So the name leaked across the hierarchy, into a design that never mentions it. Strip
the `inline$` prefix before the `$` test. The constant now gets `setMetaAnon`, and
with it a position on the user's own line instead of one inside `DFBits.scala`.

Three existing expectations had the leak baked in, all of them an enclosing
`RTDomain a` naming an anonymous constant `a` / `a_0` / `a_1`; the constants now
print inline, unnamed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Designs that one Scala declaration elaborated several of (`class Bar`
instantiated with two different parameters) were emitted as `Bar_0.sv` and
`Bar_1.sv`, one file per specialization. They now share a single `Bar.sv`.

`DB.designFileNameMap` is the one place that decides it: designs group by
their `dclMeta` namespace and position, and a group of more than one recovers
the declaration's name from its members' longest common prefix, cut back to
its last `_`. Reading the suffix off the GROUP is what tells an enumerated
`Bar_0` apart from a declared `Adder_8` (a lone design keeps its whole name),
and it covers every suffix form: elaboration's `_<n>`, `UniqueDesigns`', and
the instance-named clones `ReduplicateDesign` makes (`Foo_a`/`Foo_b` -> `Foo`).

Two constraints the AES suite pinned down:

- File names stay unique CASE-INSENSITIVELY, so the output is the same on
  every OS. AES has a `cipher` sub-design under a `Cipher` top; the recovered
  name collides, and the sub-design keeps `cipher_0` as it had before.
- A design with an UNKNOWN position is declared in no Scala source, so it
  groups only with itself rather than with every other such design.

`Printer.designFileGroups` topologically sorts the groups: post-order per
design no longer suffices once positions merge (`Top{P{Bar(22)}, Q{Bar(42)}}`
would put `P` ahead of the `Bar` file it instantiates), and VHDL analysis
order depends on it. A cycle breaks at its first back edge.

Verilator: `-Wno-DECLFILENAME` on every lint/simulate run, since a file named
after a declaration is by construction not named after the one module it
declares. Its `.vlt` waivers filtered on `-file "<dclName>.*"` and so stopped
matching once the file moved; they go through `designFileNameMap` now. A
waiver can no longer single out one design from its file-mates, which is
inherent to their sharing a file, and the `-match` pattern still narrows it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`initFile` bakes file contents into a Const, but only the body knows which
files it reads, so no cache key can carry them and a changed file was a
false hit at both cache tiers. The read now records the file on its design
as a SourceFile(External, InitFile, path, contents) that rides the sub-DB
into cache entries and the elaborated hierarchical DB, and every hit
re-reads and compares (DB.initFilesUnchanged): the design load gate before
adopting an entry (a stale child fails the parent's adoption before it
commits), and the DFApp elaborate step through factum 0.3.0's new
hit-validation hook (Task.cached's `validate`, surfaced as
DiskCache.Step.cacheHitValidator). A rejected entry re-elaborates live and
overwrites the same key, so contents stay out of the key and stale entries
never accumulate. A missing file is a miss, not an error: the live run
raises the user-facing error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@soronpo
soronpo merged commit 5977574 into main Aug 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant